package appStarshipPushups.client.model;

import java.awt.Point;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

import javax.swing.JLabel;

import appStarshipPushups.client.form.AppFrame;
import appStarshipPushups.common.Laser;
import appStarshipPushups.common.Protocole;
import appStarshipPushups.common.Starship;

/**
 * Cette classe représente une connection avec le serveur.
 * @author Adam Labranche & Kevin Arsenault
 */
public class Connection extends Thread
{
	private static final String DOT = "\\.";
	private static final String REAL_DOT = ".";
	private static final int NB_IP_LENGTH = 4;
	private static final int IP_MAX = 255;
	private static final int IP_MIN = 0;
	private static final String UNCLOSABLE_CONNECTION = "Impossible de fermer la connexion.";
	private static final String READ_FAIL = "Erreur lors de la lecture de l'entrée.";
	private static final String INVALID_MESSAGE_RECIEVED = "Message reçu invalide.";
	
	// L'instance de la connexion.
	private static Connection connectionInstance = null;
	
	// Le socket de la connexion
	private Socket soc = null;
	// La sortie de la connexion.
	private PrintWriter out = null;
	// L'entrée de la connexion.
	private BufferedReader in = null;
	// Représente l'état de la connexion.
	private boolean isConnected = false;

	/**
	 * C'est le constructeur par défaut.
	 */
	private Connection()
	{
	}
	
	/**
	 * Établir la connection au port et à l'adresse désirée.
	 * @param aAddress L'adresse IP
	 * @param aPort Le port sur la machine.
	 */
	public void startConnection(String aAddress, int aPort) throws ConnectionException
	{
		// Créer une connection et obtenir les E/S de celle-ci.
		try
		{
			this.soc = new Socket(aAddress, aPort);
			
			this.out = new PrintWriter(this.soc.getOutputStream(), true);
			this.in = new BufferedReader(new InputStreamReader(this.soc.getInputStream()));
			
			// Indiquer que la connection est établie.
			this.isConnected = true;
			this.start();
		}
		catch(IOException e)
		{
			throw new ConnectionException("Erreur dans la connection au server");
			//this.isConnected = false;
		}
	}
	
	/**
	 * Cette fonction termine la connection.
	 * @throws ConnectionException
	 */
	public void endConnection() throws ConnectionException
	{
		try
		{
			if(this.soc != null)
			{
				this.soc.close();
				this.soc = null;
			}
			if(this.out != null)
			{
				this.out.close();
				this.out = null;
			}
			if(this.in != null)
			{
				this.in.close();
				this.in = null;
			}
			this.isConnected = false;
			Connection.connectionInstance = null;
		}
		catch(IOException e)
		{
			throw new ConnectionException(Connection.UNCLOSABLE_CONNECTION);
		}
	}
	
	
	/**
	 * Envoyer un message au serveur.
	 * @param msg Le message à envoyer.
	 * @throws ConnectionException Si l'envoie du message ne fonctionne pas.
	 */
	public void sendMsg(String msg) throws ConnectionException
	{
		// Si la connection est établie.
		if(this.isConnected)
		{
			this.out.println(msg);
			//System.out.println(msg);
		}
		else // Erreur
		{
			throw new ConnectionException("Vous ne pouvez pas envoyer de l'information la connection n'est pas établie");
		}
	}
	
	/**
	 * Cette fonction sert à envoyer une demande de déplacement.
	 * @param direction La direction vers laquelle le vaisseau veut se diriger.
	 * @throws ConnectionException Si l'envoie du message ne fonctionne pas.
	 */
	public void sendMoveMessage(String direction) throws ConnectionException
	{
		try
		{
			this.sendMsg(Protocole.FUNCTION_MOVE 
					+ Protocole.SEPARATOR + direction);
		}
		catch(ConnectionException e)
		{
			throw new ConnectionException(e.getMessage());
		}
	}
	
	/**
	 * Prendre le singleton de la connection.
	 * @return L'instance de la connection.
	 */
	public static Connection getInstance()
	{
		// Si la connection n'est pas créer.
		if(Connection.connectionInstance == null)
		{
			Connection.connectionInstance = new Connection();
			System.out.println("Initialisé le singleton");
		}
		
		return Connection.connectionInstance;
	}
	
	/**
	 * S'assure que l'adresse IP en paramètre soit standard. S'il n'y a pas
	 * assez de chiffres, une chaîne de caractères vide est retournée.
	 * @param ip L'adresse IP à vérifier
	 * @return L'adresse IP arrangée
	 */
	public static String ipValidation(String ip)
	{
		String result = "";
		String[] ipSplit = ip.split(Connection.DOT);
		int[] iIP = {0, 0, 0, 0};
		// On vérifie s'il y a assez d'éléments.
		if(ipSplit.length == Connection.NB_IP_LENGTH)
		{
			// On arrange l'adresse IP pour que les nombres soient corrects.
			for(int i = 0; i < ipSplit.length; i++)
			{
				iIP[i] = Integer.parseInt(ipSplit[i]);
				if(iIP[i] > Connection.IP_MAX)
				{
					iIP[i] = Connection.IP_MAX;
				}
				else if(iIP[i] < Connection.IP_MIN)
				{
					iIP[i] = Connection.IP_MIN;
				}
				result += Integer.toString(iIP[i]) + Connection.REAL_DOT;
			}
		}
		// On enlève le dernier point de trop.
		if(result.length() > 0)
		{
			result = result.substring(0, result.length() - 1);
		}
		return result;
	}
	
	/**
	 * Cette fonction envoie un message au serveur signifiant que le joueur 
	 * quitte la partie. Du même coup, la fonction termine la connexion.
	 * @throws ConnectionException Si l'envoie du message ne fonctionne pas.
	 */
	public void sendQuit() throws ConnectionException
	{
		try
		{
			this.sendMsg(Protocole.FUNCTION_QUIT);
		}
		catch(ConnectionException e)
		{
			throw new ConnectionException(e.getMessage());
		}
	}
	
	/**
	 * Cette fonction permet d'obtenir l'index du vaisseau selon son numéro
	 * d'identification.
	 * @param id Le numéro d'identification du vaisseau à chercher.
	 * @return L'index du vaisseau dans la liste chaînée.
	 */
	private int getShipIndex(int id)
	{
		int result = -1;
		int i = 0;
		System.out.println("Nb ships : " + Protocole.getShips().size());
		while(result == -1 && i < Protocole.getShips().size())
		{
			if(Protocole.getShips().get(i).getID() == id)
			{
				result = i;
			}
			++i;
		}
		return result;
	}
	
	/**
	 * Cette fonction permet d'obtenir l'index du laser selon son numéro
	 * d'identification.
	 * @param id Le numéro d'identification du laser à chercher.
	 * @return L'index du laser dans la liste chaînée.
	 */
	private int getLaserIndex(int id)
	{
		int result = -1;
		int i = 0;
		while(result == -1 && i < Protocole.getLasers().size())
		{
			if(Protocole.getLasers().get(i).getID() == id)
			{
				result = i;
			}
			++i;
		}
		return result;
	}
	/**
	 * Exécute la boucle principale de la connection.
	 */
	public void run()
	{
		while(this.isConnected)
		{
			try
			{
				String line = this.in.readLine();
				
				// On traite le message reçu.
				if(line.startsWith(Protocole.FUNCTION_SET_PLAYER))
				{
					String[] setPlayer = line.split(Protocole.SEPARATOR_SPLIT);
					if(setPlayer.length == Protocole.SET_PLAYER_LENGTH)
					{
						// On prend les données envoyées.
						int playerID = Integer.parseInt(setPlayer[Protocole.SET_PLAYER_INDEX]);
						int playerIndex = this.getShipIndex(playerID);
						int x = Integer.parseInt(setPlayer[Protocole.SET_PLAYER_X]);
						int y = Integer.parseInt(setPlayer[Protocole.SET_PLAYER_Y]);
						int direction = Integer.parseInt(setPlayer[Protocole.SET_PLAYER_DIRECTION]);
						
						System.out.println("Player ID : " + playerID);
						System.out.println("Player Index : " + playerIndex);
						System.out.println("Player x : " + x);
						System.out.println("Player y : " + y);
						
						// Si c'est un nouveau vaisseau.
						if(playerIndex == -1)
						{
							// On ajoute le vaisseau envoyé.
							Protocole.getShips().add(new Starship(x, y, direction, playerID));
							System.out.println("Re setplayer nbShips");
							playerIndex = this.getShipIndex(playerID);
						}
						else
						{
							// Si c'est un vaisseau déjà existant, on le 
							// modifie.
							Protocole.getShips().get(playerIndex).setPosition(new Point(x, y));
							Protocole.getShips().get(playerIndex).setDirection(direction);
						}
						// On met à jour le JLabel du vaisseau.
						System.out.println("Index du joueur : " + playerIndex);
						AppFrame.getInstance().getGamePanel().setPlayer(playerID, new Point(x, y), direction);
						
						// Rafraichir le panel du jeu.
						AppFrame.getInstance().getGamePanel().repaint();
						AppFrame.getInstance().getGamePanel().validate();
					}
				}
				else if(line.startsWith(Protocole.FUNCTION_ADD_LASER))
				{
					// Décomposé la fonction d'ajouter un laser.
					String[] addLaser = line.split(Protocole.SEPARATOR_SPLIT);
					
					// Si le nombre d'argument est bon.
					if(addLaser.length == Protocole.ADD_LASER_LENGTH)
					{
						// Recevoir les informations sur le laser.
						int laserID = Integer.parseInt(addLaser[Protocole.ADD_LASER_ID]);
						
						int x1 = Integer.parseInt(addLaser[Protocole.ADD_LASER_STARTX]);
						int y1 = Integer.parseInt(addLaser[Protocole.ADD_LASER_STARTY]);
						int x2 = Integer.parseInt(addLaser[Protocole.ADD_LASER_ENDX]);
						int y2 = Integer.parseInt(addLaser[Protocole.ADD_LASER_ENDY]);
						
						// Créer le laser et l'ajouter à la liste de lasers.
						Laser addedLaser = new Laser(x1, y1, x2, y2, laserID);
						Protocole.getLasers().add(addedLaser);
					}
					
					// Rafraîchir l'écran.
					AppFrame.getInstance().getGamePanel().repaint();
					AppFrame.getInstance().getGamePanel().validate();
				}
				else if(line.startsWith(Protocole.FUNCTION_REMOVE_LASER))
				{
					System.out.println("Fonction remove laser appelée");
					
					// Décomposer la fonction d'enlever le laser.
					String[] removeLaser = line.split(Protocole.SEPARATOR_SPLIT);
					
					int laserID = Integer.parseInt(removeLaser[Protocole.REMOVE_LASER_ID]);
					int laserIndex = this.getLaserIndex(laserID);
					
					System.out.println("Index du laser : " + laserIndex);
					System.out.println("Nombre de lasers : " + Protocole.getLasers().size());
					
					// Si la fonction comporte le bon nombre d'arguments.
					if(removeLaser.length == Protocole.REMOVE_LASER_LENGTH)
					{
						// Enlever le laser du jeu.
						AppFrame.getInstance().getGamePanel().removeLaser(laserIndex);
					}
					
					// Rafraichir l'écran.
					AppFrame.getInstance().getGamePanel().repaint();
					AppFrame.getInstance().getGamePanel().validate();
				}
				else if(line.startsWith(Protocole.FUNCTION_ERASE_PLAYER))
				{
					String[] toErase = line.split(Protocole.SEPARATOR_SPLIT);
					if(toErase.length == Protocole.ERASE_PLAYER_LENGTH)
					{
						int playerID = Integer.parseInt(toErase[Protocole.ERASE_PLAYER_INDEX]);
						int playerIndex = -1;
						System.out.println(line);
						synchronized(Protocole.synchronization)
						{
							System.out.println("erasePlayer nb Ships");
							playerIndex = this.getShipIndex(playerID);
						}
						if(playerIndex != -1)
						{
							Protocole.getShips().remove(playerIndex);
							AppFrame.getInstance().getGamePanel().remove(AppFrame.getInstance().getGamePanel().getLblStarships()[playerID]);
							System.out.println("JLabels : " + AppFrame.getInstance().getGamePanel().getLblStarships().length);
							AppFrame.getInstance().getGamePanel().getLblStarships()[playerID] = new JLabel();
							AppFrame.getInstance().getGamePanel().repaint();
						}
					}
				}
				else if(line.startsWith(Protocole.FUNCTION_ACCEPT_PLAYER))
				{
					String[] playerAccepted = line.split(Protocole.SEPARATOR_SPLIT);
					if(playerAccepted.length == Protocole.ACCEPT_PLAYER_LENGTH)
					{
						// On obtient le numéro d'identification du joueur
						// principal.
						AppFrame.getInstance().getGamePanel().setMainPlayerID(Integer.parseInt(playerAccepted[Protocole.ACCEPT_PLAYER_ID]));
						
						// On signale que la partie peut commencer.
						AppFrame.getInstance().getMenu().gameStarted();
						AppFrame.getInstance().getGamePanel().startGame();
						System.out.println(line);
					}
				}
				else if(line.startsWith(Protocole.FUNCTION_SEND_ERROR))
				{
					String[] err = line.split(Protocole.SEPARATOR_SPLIT);
					if(err.length == Protocole.SEND_ERROR_MSG_INDEX + 1)
					{
						System.err.println(err[Protocole.SEND_ERROR_MSG_INDEX]);
					}
				}
				else if(line.startsWith(Protocole.FUNCTION_LEAVEOK))
				{
					try
					{
						this.endConnection();
						AppFrame.getInstance().getGamePanel().endGame();
						AppFrame.getInstance().getMenu().gameEnded();
					}
					catch(ConnectionException e)
					{
						System.err.println(e.getMessage());
					}					
				}
				else if(line.startsWith("bob"))
				{
					System.out.println("\n\n BOB \n\n");
				}
				else
				{
					System.err.println(Connection.INVALID_MESSAGE_RECIEVED);
				}
			}
			catch(IOException e)
			{
				System.err.println(Connection.READ_FAIL);
				try
				{
					this.endConnection();
				}
				catch(ConnectionException ex)
				{
					System.err.println(ex.getMessage());
				}
				AppFrame.getInstance().getGamePanel().endGame();
				AppFrame.getInstance().getMenu().gameEnded();
			}
		}
	}
	
	/**
	 * Cette fonction permet de terminer la connexion.
	 */
	public void end()
	{
		try
		{
			this.endConnection();
		}
		catch(ConnectionException e)
		{
			System.err.println(e.getMessage());
		}
	}
}
